#include "LogicEngine.hpp"

const float LogicEngine::ATTRACTION_VELOCITY = 2.0f;
const float LogicEngine::GRAB_RADIUS = 1.0f;

LogicEngine::LogicEngine(ControlManager* controlManager) {
    this->controlManager = controlManager;
}

void LogicEngine::end() {
    delete this->contactListener;
}

void LogicEngine::init(Scene* scene, float elapsedTime) {
    this->contactListener = new LogicEngine::ContactListener();
    this->elapsedTimeFromExitTouching = 0.0f;
    scene->world->SetContactListener(this->contactListener);
    this->lastHeroY = scene->hero->body->GetPosition().y;

    // Hero intro.
    scene->start->setState(false);
    scene->start->setFutureState(true, 1.0f);      // Close the start door after 1 second.
    this->firstTime = elapsedTime;
    this->lastTime = elapsedTime;
    scene->hero->body->ApplyLinearImpulse(b2Vec2(250.0f, 0.0f), scene->hero->body->GetWorldCenter());
}

bool LogicEngine::isSceneCompleted() {
    return (this->elapsedTimeFromExitTouching >= 2.0f);      // Wait 2 seconds before end the scene after completion.
}

void LogicEngine::manageHero(Scene* scene) {
    if (this->lastTime - this->firstTime < 1.0f) {      // Wait 1 second before allow input.
        return;
    }

    if (this->contactListener->exitTouched) {
        return;
    }

    float currentHeroY = scene->hero->body->GetPosition().y;
    scene->hero->onAir = (this->contactListener->numberOfFootCollisions == 0);
    scene->hero->flying = (scene->hero->body->GetGravityScale() == 0.0f);

    // Left, right.
    float moveImpulse = (scene->hero->onAir) ? 4.0f : 8.0f;

    // No move
    if (!this->controlManager->goLeft() && !this->controlManager->goRight())
            scene->hero->stopping = true;
    else scene->hero->stopping = false;

    if (this->controlManager->goLeft()) {
        b2Vec2 impulse(-moveImpulse, 0.0f);
        scene->hero->body->ApplyLinearImpulse(impulse, scene->hero->body->GetWorldCenter());

        if (scene->hero->grabbedBox == NULL) {
            scene->hero->lookingAtRight = false;
        }
    }

    if (this->controlManager->goRight()) {
        b2Vec2 impulse(moveImpulse, 0.0f);
        scene->hero->body->ApplyLinearImpulse(impulse, scene->hero->body->GetWorldCenter());

        if (scene->hero->grabbedBox == NULL) {
            scene->hero->lookingAtRight = true;
        }
    }

    // Up, down.

    if (scene->hero->flying) {
        if (this->controlManager->goUp()) {
            b2Vec2 impulse(0.0f, 4.0f);
            scene->hero->body->ApplyLinearImpulse(impulse, scene->hero->body->GetWorldCenter());
        }

        if (this->controlManager->goDown()) {
            b2Vec2 impulse(0.0f, -4.0f);
            scene->hero->body->ApplyLinearImpulse(impulse, scene->hero->body->GetWorldCenter());
        }
    }

    // Jump.

    if (!scene->hero->onAir && this->controlManager->jump() && (scene->hero->mainFixture->GetRestitution() == HeroElement::RESTITUTION || this->lastHeroY == currentHeroY)) {
        b2Vec2 impulse(0.0f, 350.0f);
        scene->hero->body->ApplyLinearImpulse(impulse, scene->hero->body->GetWorldCenter());
    }

    // Grab box.

    if (this->controlManager->doAction()) {
        if (scene->hero->grabbedBox != NULL) {
            scene->world->DestroyJoint(scene->hero->grabbedBox);
            scene->hero->grabbedBox = NULL;
        }
        else {
            b2Vec2 heroPosition = scene->hero->body->GetPosition();
            BoxElement* boxAtTheMinDistance = NULL;
            float minSquaredDistance = LogicEngine::GRAB_RADIUS * LogicEngine::GRAB_RADIUS;

            for (vector<BoxElement*>::iterator i = scene->boxElements->begin(); i != scene->boxElements->end(); ++i) {
                BoxElement* boxElement = *i;
                b2Vec2 boxPosition = boxElement->body->GetPosition();

                if ((scene->hero->lookingAtRight && boxPosition.x >= heroPosition.x) || (!scene->hero->lookingAtRight && boxPosition.x <= heroPosition.x)) {
                    float squaredDistance = (boxPosition - heroPosition).LengthSquared();

                    if (squaredDistance <= minSquaredDistance) {
                        RayCastCallback callback;
                        scene->world->RayCast(&callback, heroPosition, boxPosition);

                        if (callback.numberOfCollisions == 1) {
                            boxAtTheMinDistance = boxElement;
                            minSquaredDistance = squaredDistance;
                        }
                    }
                }
            }

            if (boxAtTheMinDistance != NULL) {
                b2DistanceJointDef jointDefinition;
                jointDefinition.Initialize(scene->hero->body, boxAtTheMinDistance->body, scene->hero->body->GetWorldCenter(), boxAtTheMinDistance->body->GetWorldCenter());
                jointDefinition.collideConnected = true;
                scene->hero->grabbedBox = scene->world->CreateJoint(&jointDefinition);
            }
        }
    }

    this->lastHeroY = currentHeroY;
}

/*void LogicEngine::movePlatform(MoveablePlatformElement* moveablePlatform) {
    if (moveablePlatform->shouldBeMoved) {
        b2Vec2 linearVelocity(moveablePlatform->linearVelocity->x, moveablePlatform->linearVelocity->y);

        if (!moveablePlatform->isActuated()) {
            linearVelocity *= -1.0f;
        }

        //moveablePlatform->body->SetLinearVelocity(linearVelocity);
    }
}*/

void LogicEngine::render(Scene* scene, float elapsedTime) {
    if (this->lastTime == this->firstTime) {
        // Skip the first frame to prevent physic problems.
        this->lastTime = elapsedTime;
    }
    else {
        float elapsedTimeInThisFrame = elapsedTime - this->lastTime;

        if (scene->room->roomNumber == 16 && (elapsedTime - this->firstTime) > 7.0f) {          // End the last scene after 7 + 2 seconds.
            this->contactListener->exitTouched = true;
        }

        if (this->contactListener->exitTouched) {
            this->elapsedTimeFromExitTouching += elapsedTimeInThisFrame;
        }

        /*for (vector<MoveablePlatformElement*>::iterator i = scene->moveablePlatformElements->begin(); i != scene->moveablePlatformElements->end(); ++i) {
            this->movePlatform(*i);
        }*/

        scene->world->Step(elapsedTimeInThisFrame, 8, 3);

        for (vector<ActionableElement*>::iterator i = scene->actionableElements->begin(); i != scene->actionableElements->end(); ++i) {
            ActionableElement* element = *i;
            element->triggerFunctions(elapsedTimeInThisFrame);
        }

        this->manageHero(scene);
        this->lastTime = elapsedTime;
    }
}

void LogicEngine::setFriction(Element* element, float friction) {
    b2Fixture* i = element->body->GetFixtureList();

    if (i->GetFriction() == friction) {
        return;
    }

    while (i != NULL) {
        i->SetFriction(friction);
        i = i->GetNext();
    }

    for (b2ContactEdge* j = element->body->GetContactList(); j != NULL; j = j->next) {
        j->contact->SetFriction(friction);
    }
}

void LogicEngine::setRestitution(Element* element, float restitution) {
    b2Fixture* i = element->body->GetFixtureList();

    if (i->GetRestitution() == restitution) {
        return;
    }

    while (i != NULL) {
        i->SetRestitution(restitution);
        i = i->GetNext();
    }
}

// ContactListener.

LogicEngine::ContactListener::ContactListener() {
    this->exitTouched = false;
    this->numberOfFootCollisions = 0;
}

void LogicEngine::ContactListener::BeginContact(b2Contact* contact) {
    Element* elementA = (Element*)contact->GetFixtureA()->GetBody()->GetUserData();
    Element* elementB = (Element*)contact->GetFixtureB()->GetBody()->GetUserData();

    if (elementA->isSwitch()) {
        this->beginContactWithSwitch((SwitchElement*)elementA, elementB);
    }
    else if (elementB->isSwitch()) {
        this->beginContactWithSwitch((SwitchElement*)elementB, elementA);
    }

    if (elementA->isFlash()) {
        this->beginContactWithFlash((FlashElement*)elementA, elementB);
    }
    else if (elementB->isFlash()) {
        this->beginContactWithFlash((FlashElement*)elementB, elementA);
    }

    // Jump sensor.

    if (elementA->isHero() && contact->GetFixtureA()->IsSensor()) {
        if (!elementB->isFlash() && (!elementB->isLight() || ((LightElement*)elementB)->lightType == LightElement::solidLight)) {
            ++this->numberOfFootCollisions;
        }
    }
    else if (elementB->isHero() && contact->GetFixtureB()->IsSensor()) {
        if (!elementB->isFlash() && (!elementA->isLight() || ((LightElement*)elementA)->lightType == LightElement::solidLight)) {
            ++this->numberOfFootCollisions;
        }
    }

    // Exit sensor.

    if (elementA->isExit()) {
        this->beginContactWithExit((ExitElement*)elementA, elementB);
    }
    else if (elementB->isExit()) {
        this->beginContactWithExit((ExitElement*)elementB, elementA);
    }
}

void LogicEngine::ContactListener::beginContactWithExit(ExitElement* exitElement, Element* element) {
    if (element->isHero()) {
        this->exitTouched = true;
        b2Vec2 impulse(exitElement->impulseDirection->x, exitElement->impulseDirection->y);
        impulse *= 100.0f;
        element->body->ApplyLinearImpulse(impulse, element->body->GetWorldCenter());
    }
}

void LogicEngine::ContactListener::beginContactWithFlash(FlashElement* flashElement, Element* element) {
    if (element->isHero() || element->isBox()) {
        ++flashElement->numberOfContacts;
        element->body->SetLinearVelocity(b2Vec2(0.0f, 0.0f));
        element->body->ApplyLinearImpulse(*flashElement->impulse, element->body->GetWorldCenter());
    }
}

void LogicEngine::ContactListener::beginContactWithSwitch(SwitchElement* switchElement, Element* element) {
    if (element->isHero() || element->isBox()) {
        if (switchElement->numberOfContacts == 0) {
            for (vector<ActionableElement*>::iterator i = switchElement->actionableElements->begin(); i != switchElement->actionableElements->end(); ++i) {
                ((ActionableElement*)*i)->toogle();
            }
        }

        ++switchElement->numberOfContacts;
    }
}

void LogicEngine::ContactListener::EndContact(b2Contact* contact) {
    b2Body* bodyA = contact->GetFixtureA()->GetBody();
    bodyA->SetGravityScale(1.0f);
    bodyA->SetLinearDamping(0.0f);
    b2Body* bodyB = contact->GetFixtureB()->GetBody();
    bodyB->SetGravityScale(1.0f);
    bodyB->SetLinearDamping(0.0f);

    Element* elementA = (Element*)bodyA->GetUserData();
    Element* elementB = (Element*)bodyB->GetUserData();

    if (elementA->isBox()) {
        LogicEngine::setRestitution(elementA, BoxElement::RESTITUTION);
        LogicEngine::setFriction(elementA, BoxElement::FRICTION);
    }
    else if (elementA->isHero()) {
        LogicEngine::setRestitution(elementA, HeroElement::RESTITUTION);
        LogicEngine::setFriction(elementA, HeroElement::FRICTION);
    }

    if (elementB->isBox()) {
        LogicEngine::setRestitution(elementB, BoxElement::RESTITUTION);
        LogicEngine::setFriction(elementB, BoxElement::FRICTION);
    }
    else if (elementB->isHero()) {
        LogicEngine::setRestitution(elementB, HeroElement::RESTITUTION);
        LogicEngine::setFriction(elementB, HeroElement::FRICTION);
    }

    if (elementA->isSwitch()) {
        this->endContactWithSwitch((SwitchElement*)elementA, elementB);
    }
    else if (elementB->isSwitch()) {
        this->endContactWithSwitch((SwitchElement*)elementB, elementA);
    }

    if (elementA->isFlash()) {
        this->endContactWithFlash((FlashElement*)elementA, elementB);
    }
    else if (elementB->isFlash()) {
        this->endContactWithFlash((FlashElement*)elementB, elementA);
    }

    // Jump sensor.

    if (elementA->isHero() && contact->GetFixtureA()->IsSensor()) {
        if (!elementB->isFlash() && (!elementB->isLight() || ((LightElement*)elementB)->lightType == LightElement::solidLight)) {
            --this->numberOfFootCollisions;
        }
    }
    else if (elementB->isHero() && contact->GetFixtureB()->IsSensor()) {
        if (!elementB->isFlash() && (!elementA->isLight() || ((LightElement*)elementA)->lightType == LightElement::solidLight)) {
            --this->numberOfFootCollisions;
        }
    }
}

void LogicEngine::ContactListener::endContactWithFlash(FlashElement* flashElement, Element* element) {
    if (element->isHero() || element->isBox()) {
        --flashElement->numberOfContacts;
    }
}

void LogicEngine::ContactListener::endContactWithSwitch(SwitchElement* switchElement, Element* element) {
    if (element->isHero() || element->isBox()) {
        --switchElement->numberOfContacts;

        if (switchElement->numberOfContacts == 0) {
            for (vector<ActionableElement*>::iterator i = switchElement->actionableElements->begin(); i != switchElement->actionableElements->end(); ++i) {
                ((ActionableElement*)*i)->toogle();
            }
        }
    }
}

void LogicEngine::ContactListener::PreSolve(b2Contact* contact, const b2Manifold* oldManifold) {
    Element* elementA = (Element*)contact->GetFixtureA()->GetBody()->GetUserData();
    Element* elementB = (Element*)contact->GetFixtureB()->GetBody()->GetUserData();

    if (elementA->isLight()) {
        this->preSolveWithSpotlight(contact, (LightElement*)elementA, elementB);
    }
    else if (elementB->isLight()) {
        this->preSolveWithSpotlight(contact, (LightElement*)elementB, elementA);
    }
}

void LogicEngine::ContactListener::preSolveWithSpotlight(b2Contact* contact, LightElement* light, Element* element) {
    if (light->lightType != LightElement::solidLight) {
        contact->SetEnabled(false);
        b2Vec2 elementPosition = element->body->GetPosition();

        if (light->shape->TestPoint(light->body->GetTransform(), elementPosition)) {
            if (light->lightType == LightElement::attractiveLight || light->lightType == LightElement::repulsiveLight) {
                // Disabling gravity and applying damping.
                element->body->SetGravityScale(0.0f);
                element->body->SetLinearDamping(1.0f);

                // Setting velocity.
                b2Vec2 velocity;

                if (light->lightType == LightElement::attractiveLight) {
                    velocity = light->body->GetPosition() - elementPosition;
                    velocity.Normalize();
                    velocity *= LogicEngine::ATTRACTION_VELOCITY;
                }
                else {
                    float angle = light->body->GetAngle();
                    velocity.Set(LogicEngine::ATTRACTION_VELOCITY * cosf(angle), LogicEngine::ATTRACTION_VELOCITY * sinf(angle));
                }

                element->body->ApplyLinearImpulse(velocity, element->body->GetWorldCenter());
            }
            else if (light->lightType == LightElement::elasticLight) {
                if (element->isBox() || element->isHero()) {
                    LogicEngine::setRestitution(element, 1.0f);
                }
            }
            else if (light->lightType == LightElement::slipperyLight) {
                if (element->isBox() || element->isHero()) {
                    LogicEngine::setFriction(element, 0.0f);
                }
            }
        }
        else {
            element->body->SetGravityScale(1.0f);
            element->body->SetLinearDamping(0.0f);

            if (element->isBox()) {
                LogicEngine::setRestitution(element, BoxElement::RESTITUTION);
                LogicEngine::setFriction(element, BoxElement::FRICTION);
            }
            else if (element->isHero()) {
                LogicEngine::setRestitution(element, HeroElement::RESTITUTION);
                LogicEngine::setFriction(element, HeroElement::FRICTION);
            }
        }
    }
}

void LogicEngine::setLastTime(float time) {
    this->lastTime = time;
}

// RayCastCallback.

LogicEngine::RayCastCallback::RayCastCallback() {
    this->numberOfCollisions = 0;
}

float LogicEngine::RayCastCallback::ReportFixture(b2Fixture* fixture, const b2Vec2 &point, const b2Vec2 &normal, float fraction) {
    ++this->numberOfCollisions;
    return 1.0f;
}
